from typing import Optional, TypeVar, Type, cast

class SpecError(Exception):
  pass

T = TypeVar('T')

def typename(type: type) -> str:
    CONTENT_TYPE_NAMES = {"dict": "Map", "str": "String",
                          "int": "Integer", "bool": "Boolean", "list": "List"}
    if type.__name__ not in CONTENT_TYPE_NAMES:
        return type.__name__
    return CONTENT_TYPE_NAMES[type.__name__]

def _get_dict(d: dict, k: str, w: str, def_val: Optional[T], expected_type: Type[T]) -> T:
  if def_val is None and k not in d:
    raise SpecError(f"{w}.{k} is mandatory, but not found")
  
  val  = d.get(k, def_val)
  if not isinstance(val, expected_type):
      raise SpecError(
          f"{w}.{k} expected to be a {typename(expected_type)} but is {typename(type(val)) if val is not None else 'not set'}")
  return cast(T, val)

def get_dict(d: dict, k: str, w: str, def_val: Optional[dict] = None) -> dict:
  return _get_dict(d, k, w, def_val, dict)

def get_str(d: dict, k: str, w: str, def_val: Optional[str] = None) -> str:
  return _get_dict(d, k, w, def_val, str)

def get_int(d: dict, k: str, w: str, def_val: Optional[int] = None) -> int:
  return _get_dict(d, k, w, def_val, int)

def get_float(d: dict, k: str, w: str, def_val: Optional[float] = None) -> float:
  return _get_dict(d, k, w, def_val, float)

def get_bool(d: dict, k: str, w: str, def_val: Optional[bool] = None) -> bool:
  return _get_dict(d, k, w, def_val, bool)

def get_list(d: dict, k: str, w: str, def_val: Optional[list] = None, item_t: Optional[type] = None) -> list:
  l = _get_dict(d, k, w, def_val, list)
  if l and item_t is not None:
    for i, elem in enumerate(l):
      if not isinstance(elem, item_t):
        raise SpecError(f"{w}.{k}[{i}] expected to be a {typename(item_t)} but is {typename(type(elem))}")
  return l